﻿using System;
using System.Collections;
using System.Collections.Generic;
using FitNesseRunner.Model;
using System.Linq;

namespace FitNesseRunner
{
	public class QueryTableHandler : ITableHandler
	{
		private readonly IReflectionProvider _reflectionProvider;
		private readonly IReporter _reporter;
		private readonly IContext _context;

		public QueryTableHandler(IReporter reporter, IContext context)
		{
			_reporter = reporter;
			_context = context;
			_reflectionProvider = _context.ReflectionProvider;
		}

		private const string Prefix = "query:";

		public void Handle(ITable table)
		{
			var rawClassName = table.Cells.At(0, 0).Trim().Substring(Prefix.Length);
			var className = ExecutableTable.ToPascalCase(rawClassName);
			var parameters = table.Cells.First().Skip(1);

			if (table.Cells.Count() < 2)
			{
				_reporter.TestError("Query table must have at least 2 rows");
				return;
			}

			if (RowsHaveMissingCells(table))
			{
				_reporter.TestError("Rows have missing cells");
			}

			var instance = CreateInstance(className, parameters);
			if (instance == null)
				return;

			const string methodName = "Query";
			var arguments = Enumerable.Empty<string>();
			object output;
			try
			{
				output = _reflectionProvider.Invoke(instance, methodName, arguments);
			}
			catch (Exception ex)
			{
				_reporter.InvocationFailed(instance.GetType().Name, methodName, arguments, ex);
				return;
			}

			if (output == null || !(output is IList))
			{
				_reporter.InvocationError("Query method must return List<List<List<object>>>", instance.GetType(), methodName, arguments);
				return;
			}

			VerifyResults((IList)output, table, instance);
		}

		private static bool RowsHaveMissingCells(ITable table)
		{
			var numberOfHeaders = table.Cells.ElementAt(1).Count();
			return table.Cells.Skip(2).Any(row => row.Count() < numberOfHeaders);
		}

		private void VerifyResults(IList output, ITable table, object instance)
		{
			var idColumnName = GetIdColumnName(table);
			foreach (var expectedRow in table.Cells.Skip(2))
			{
				var expectedValue = expectedRow.First().Trim();
				var foundRow = FindRowWithId(output, idColumnName, expectedValue);
				if (foundRow != null)
					VerifyRow(foundRow, expectedRow, table, instance);
				else
					_reporter.Failed(instance, "Query", expectedValue, "(missing)");
			}
		}

		private static string GetIdColumnName(ITable table)
		{
			return table.Cells.At(1, 0).Trim();
		}

		private static IList FindRowWithId(IList output, string idColumnName, string expectedValue)
		{
			return (from row in output.Cast<IList>()
			        let idValue = GetIdValue(row, idColumnName)
			        where idValue != null && CompareToString(idValue, expectedValue)
			        select row).FirstOrDefault();
		}

		private static object GetIdValue(IList row, string idColumnName)
		{
			var idField = row.Cast<IList>().FirstOrDefault(field => (string)field[0] == idColumnName);
			return idField != null ? idField[1] : null;
		}

		private static bool CompareToString(object value, string expectedStringValue)
		{
			var expectedValue = Convert.ChangeType(expectedStringValue, value.GetType());
			return value.Equals(expectedValue);
		}

		private void VerifyRow(IList foundRow, IEnumerable<string> expectedRow, ITable table, object instance)
		{
			var idValue = GetIdValue(foundRow, GetIdColumnName(table));
			_reporter.Pass("Row found with {0}={1}", foundRow.Cast<IList<object>>().First().First(), idValue);
			
			for (var i = 0; i < table.Cells.ElementAt(1).Count(); i++)
			{
				var headerCell = table.Cells.At(1, i).Trim();
				var methodName = string.Format("Query (row id={0}, field={1})", idValue, headerCell);
				var expectedValue = expectedRow.ElementAt(i).Trim();
				if (!foundRow.Cast<IList<object>>().Any(cell => (string)cell.First() == headerCell))
				{
					_reporter.Failed(instance, methodName, expectedValue, "(field not present)");
				}
				else
				{
					var actualValue = foundRow.Cast<IList<object>>().Single(cell => (string)cell.First() == headerCell).Last();
					if (!CompareToString(actualValue, expectedValue))
						_reporter.Failed(instance, methodName, expectedValue, actualValue);
					else
					{
						_reporter.Pass("Row id='{0}', field='{1}', value='{2}'", idValue, headerCell, actualValue)
						;
					}
				}
			}
		}

		private object CreateInstance(string className, IEnumerable<string> parameters)
		{
			var variablesResolver = new VariablesResolver(_context.Variables);
			_reporter.Constructing(className);
			try
			{
				return _reflectionProvider.CreateInstance(className, parameters.Select(variablesResolver.Resolve));
			}
			catch (Exception ex)
			{
				_reporter.FailedToConstruct(className, ex);
				return null;
			}
		}

		public bool CanHandle(ITable table)
		{
			return table.Cells.At(0, 0).Trim().ToLower().StartsWith(Prefix);
		}
	}
}